spb/social-runtime-crawler
Public
TypeScript 91.8%
HTML 3.2%
JavaScript 3%
SQL 1.4%
CSS 0.7%
1"use client";23import { use } from "react";4import Link from "next/link";5import { ExternalLink } from "lucide-react";6import { useApi } from "@/lib/api";7import { fmtDate, fmtDuration, fmtNum, truncate } from "@/lib/format";8import { Empty, Metric, Panel, Skeleton, SurfaceChips, Table, Tag, TypeTag } from "@/components/ui";9import type { EntityRow } from "@/components/EntityTable";1011interface Detail {12 entity: EntityRow & { first_seen: string; last_seen: string; seen_count: number; first_session: string };13 observations: { session_id: string; step: number; ts: string; surfaces: string[]; snapshot: { name?: string; author?: string; metrics?: Record<string, number>; context?: string; provenance?: { surface: string; confidence: number; detail?: string }[] }; goal: string | null }[];14 relations: { rel_type: string; from_fp: string; to_fp: string; from_name: string | null; to_name: string | null; from_type: string; to_type: string; step: number }[];15 media: { duration_s: number | null; width: number | null; height: number | null; delivery: { kind: string; hostnames: string[] } | null; frames: string[] | null; thumbnail_url: string | null } | null;16 feed: { session_id: string; step: number; page_type: string; feed_position: number; visible: boolean; ts: string }[];17}1819export default function EntityPage({ params }: { params: Promise<{ fp: string }> }) {20 const { fp } = use(params);21 const { data, loading, error } = useApi<Detail>(`/entities/${encodeURIComponent(decodeURIComponent(fp))}`);22 if (loading) return <Skeleton rows={10} />;23 if (error || !data) return <Empty>{error ?? "not found"}</Empty>;24 const e = data.entity;25 const type = e.entity_type ?? e.type ?? "?";26 return (27 <div className="space-y-4">28 <div className="text-xs text-dim mono">29 <Link href="/entities" className="hover:text-cyan">entities</Link> / {e.fingerprint}30 </div>31 <div className="flex flex-wrap items-start gap-4">32 <div className="min-w-0">33 <div className="flex items-center gap-2">34 <TypeTag type={type} />35 <Tag>{e.platform}</Tag>36 {e.url && (37 <a href={e.url} target="_blank" rel="noreferrer" className="text-dim hover:text-cyan flex items-center gap-1 text-xs">38 open on platform <ExternalLink className="h-3.5 w-3.5" />39 </a>40 )}41 </div>42 <h1 className="text-xl font-semibold tracking-tight mt-2 max-w-3xl">{e.name ?? e.text_excerpt ?? e.platform_id}</h1>43 {e.author && <div className="text-sm text-fg-2 mt-1">by {e.author}</div>}44 {e.text_excerpt && e.name && <p className="text-sm text-fg-2 mt-2 max-w-3xl">{e.text_excerpt}</p>}45 </div>46 </div>4748 <div className="grid grid-cols-1 xl:grid-cols-3 gap-4">49 <Panel title="Evidence per field" sub="every canonical value points back to the surface that produced it (§38)" className="xl:col-span-2" pad={false}>50 <div className="p-4">51 <Table head={["field", "value", "evidence"]} dense>52 {Object.entries(e.fields ?? {}).map(([k, f]) => (53 <tr key={k}>54 <td className="mono text-fg-2 whitespace-nowrap">{k}</td>55 <td className="max-w-[480px] break-words">{typeof f.value === "string" ? f.value : JSON.stringify(f.value)}</td>56 <td>57 <SurfaceChips provenance={f.provenance} />58 {f.provenance?.[0]?.detail && <div className="text-[10px] text-dim mono truncate max-w-[280px]">{f.provenance[0].detail}</div>}59 </td>60 </tr>61 ))}62 </Table>63 </div>64 </Panel>65 <div className="space-y-4">66 <Panel title="Canonical record">67 <Metric label="platform id" value={e.platform_id ?? "—"} />68 <Metric label="confidence" value={e.confidence ? Number(e.confidence).toFixed(2) : "—"} />69 <Metric label="observed" value={`${e.seen_count}× in ${new Set(data.observations.map((o) => o.session_id)).size} session(s)`} />70 <Metric label="first seen" value={fmtDate(e.first_seen)} />71 <Metric label="last seen" value={fmtDate(e.last_seen)} />72 {e.metrics &&73 Object.entries(e.metrics).map(([k, v]) => (74 <Metric key={k} label={k} value={fmtNum(v)} />75 ))}76 {data.media && (77 <>78 <Metric label="duration" value={fmtDuration(data.media.duration_s)} />79 {data.media.width ? <Metric label="resolution" value={`${data.media.width}×${data.media.height}`} /> : null}80 {data.media.delivery && <Metric label="delivery" value={`${data.media.delivery.kind} · ${data.media.delivery.hostnames.slice(0, 2).join(", ")}`} />}81 </>82 )}83 </Panel>84 {data.media?.frames?.length ? (85 <Panel title="Sampled frames" sub="0 / 25 / 50 / 75 / 100 % (LEVEL 2)">86 <div className="grid grid-cols-5 gap-1">87 {data.media.frames.map((f) => (88 // eslint-disable-next-line @next/next/no-img-element89 <img key={f} src={`/media/${f.split("/media/")[1] ?? ""}`} alt="" className="aspect-video object-cover rounded" />90 ))}91 </div>92 </Panel>93 ) : data.media?.thumbnail_url || e.media?.thumbnail_url ? (94 <Panel title="Thumbnail">95 {/* eslint-disable-next-line @next/next/no-img-element */}96 <img src={data.media?.thumbnail_url ?? e.media?.thumbnail_url ?? ""} alt="" className="rounded w-full" />97 </Panel>98 ) : null}99 </div>100 </div>101102 <div className="grid grid-cols-1 xl:grid-cols-2 gap-4">103 <Panel title="Observations" sub="raw sightings, never overwritten (§37)" pad={false}>104 <div className="p-4">105 <Table head={["when", "session", "step", "surfaces", "context", "name as seen"]} dense>106 {data.observations.map((o, i) => (107 <tr key={i}>108 <td className="mono text-dim whitespace-nowrap">{fmtDate(o.ts)}</td>109 <td>110 <Link href={`/sessions/${o.session_id}`} className="mono text-[11px] hover:text-cyan">111 {truncate(o.goal ?? o.session_id, 32)}112 </Link>113 </td>114 <td className="mono">#{o.step}</td>115 <td>116 <SurfaceChips provenance={o.snapshot.provenance ?? o.surfaces.map((s) => ({ surface: s, confidence: 0 }))} />117 </td>118 <td className="text-dim text-[11px]">{o.snapshot.context}</td>119 <td className="text-fg-2 max-w-[260px] truncate">{o.snapshot.name}</td>120 </tr>121 ))}122 </Table>123 </div>124 </Panel>125 <div className="space-y-4">126 <Panel title="Relationships" pad={false}>127 <div className="p-4">128 {data.relations.length ? (129 <Table head={["from", "relation", "to"]} dense>130 {data.relations.map((r, i) => (131 <tr key={i}>132 <td className="max-w-[240px] truncate">133 <TypeTag type={r.from_type ?? "?"} /> <Link href={`/entities/${encodeURIComponent(r.from_fp)}`} className="hover:text-cyan">{truncate(r.from_name ?? r.from_fp, 40)}</Link>134 </td>135 <td>136 <Tag color="#9d7bff">{r.rel_type}</Tag>137 </td>138 <td className="max-w-[240px] truncate">139 <TypeTag type={r.to_type ?? "?"} /> <Link href={`/entities/${encodeURIComponent(r.to_fp)}`} className="hover:text-cyan">{truncate(r.to_name ?? r.to_fp, 40)}</Link>140 </td>141 </tr>142 ))}143 </Table>144 ) : (145 <Empty>no relationships recorded</Empty>146 )}147 </div>148 </Panel>149 <Panel title="Feed exposure" sub="where it ranked when the crawler saw it (§44)" pad={false}>150 <div className="p-4">151 {data.feed.length ? (152 <Table head={["when", "page", "position", "visible"]} dense>153 {data.feed.map((f, i) => (154 <tr key={i}>155 <td className="mono text-dim whitespace-nowrap">{fmtDate(f.ts)}</td>156 <td>157 <Tag color="#38e1ff">{f.page_type}</Tag>158 </td>159 <td className="mono">#{f.feed_position + 1}</td>160 <td className="mono">{f.visible ? "yes" : "below fold"}</td>161 </tr>162 ))}163 </Table>164 ) : (165 <Empty>not seen in a feed</Empty>166 )}167 </div>168 </Panel>169 </div>170 </div>171 </div>172 );173}174